package sk.tuke.constraintbuilder.analyzer;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.bind.JAXBException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.stream.StreamResult;

import org.xml.sax.InputSource;

import sk.tuke.constraintbuilder.model.*;
import sk.tuke.constraintbuilder.utils.MyUtils;
import sk.tuke.constraintbuilder.utils.MyPreferences;


public class SchemaGenerator {
		
	public File generateXmlSchema(Set<Ruleset> rulesets, String name){
		StringBuilder schema = new StringBuilder("<?xml version='1.0' encoding='utf-8'?>");
		schema.append("<schema xmlns=\"http://purl.oclc.org/dsdl/schematron\">");
		for (Ruleset ruleset : rulesets) {
			for (Constraint constraint : ruleset.getConstraints()) {
				schema.append(generatePattern(constraint));
			}
		}
		schema.append("</schema>");
		return saveToFile(schema.toString(), name);
	}
	
	private String generatePattern(Constraint constraint){		
		String context = generateContext(constraint);
		List<String> asserts = generateAsserts(constraint);
		StringBuilder pattern = new StringBuilder("<pattern name=\""+constraint.getName()+"\">");
		pattern.append("<rule context=\""+context+"\">");
		for (String string : asserts) {
			pattern.append(string);
		}
		pattern.append("</rule>");
		pattern.append("</pattern>");
		return pattern.toString();
	}

	private String generateContext(Constraint constraint) {
		Context context = constraint.getContext();
		if(context==null || context==Context.UNDEFINED){
			return "//annotation[@name='"+constraint.getAnnotationName()+"']";
		}else{
			return "//"+context.name().toLowerCase()+"/annotation[@name='"+constraint.getAnnotationName()+"']";
		}
	}
	
	private List<String> generateAsserts(Constraint constraint) {
		List<String> asserts = new ArrayList<>();
		String position = "#<value-of select=\"@line\"/>#<value-of select=\"@startChar\"/>#<value-of select=\"@endChar\"/>";
		position += "#"+constraint.getSeverity();
	
		RulesElement rule = constraint.getRules();
		List<RuleElement> elements = rule.getElements();
		
		for (RuleElement element : elements) {
			IElementOfStructuredElement subElement = null;
			if(element instanceof StructuredRuleElement)
				subElement = ((StructuredRuleElement)element).getElement();
			StringBuilder assertBuilder = new StringBuilder();
			assertBuilder.append("<assert test=\"");
			assertBuilder.append(generateCondXpath(constraint));
			assertBuilder.append(generateRuleXpath(constraint, element));
			assertBuilder.append("\">"+generateErrorMessage(element)+position);
			assertBuilder.append("</assert>");
			asserts.add(assertBuilder.toString());
		}
		return asserts;
	}
	
	private String generateRuleXpath(Constraint constraint, RuleElement element){
		ConstraintPatterns patterns;
		try {
			patterns = MyUtils.getConstraintPatterns();
			String elementName = element.getName();
			String xPath = null;
			if(element instanceof SimpleRuleElement){
				String context = generateContext(constraint);
				String value = String.valueOf(((SimpleRuleElement)element).getValue());
				xPath = patterns.getXpath(elementName);
				xPath = xPath.replace("$context", context);
				xPath = xPath.replace("$value", value);
			}
			else if(element instanceof StructuredRuleElement){
				IElementOfStructuredElement subElement = ((StructuredRuleElement)element).getElement();
				int minOccurs = 1, maxOccurs = 1000;
				String elementContext = "*/*";
				String value = null;
				if(subElement instanceof Value){
					value = ((Value)subElement).toString();
					xPath = patterns.getXpath(elementName);
					xPath = xPath.replace("$value", value);
				}
				else if(subElement instanceof INodeElement){
					Iterator<ILeaf> iterator = ((INodeElement)subElement).getElements().iterator();
					xPath = "";
					String logic = " or ";
					if(subElement instanceof Annotations)logic = " and ";
					if(subElement instanceof Not)xPath += "not(";
					while (iterator.hasNext()) {
						ILeaf leaf = iterator.next();
						xPath += patterns.getXpath(elementName);
						xPath = xPath.replace("$value", leaf.toString());
						if(leaf instanceof Annotation){
							minOccurs = ((Annotation)leaf).getMinOccurs()!=null ? ((Annotation)leaf).getMinOccurs().intValue() : 1;
							Context context = ((Annotation)leaf).getContext();
							elementContext = context==Context.METHOD ? "methods/method" : context==Context.FIELD ? "fields/field" : "*/*";
							xPath = xPath.replace("$context", elementContext);
							xPath = xPath.replace("$minoccurs", String.valueOf(minOccurs));
							xPath = xPath.replace("$maxoccurs", String.valueOf(maxOccurs));
						}
						if(iterator.hasNext()) xPath += logic;
					}
					if(subElement instanceof Not)xPath += ")";
				}
			}
			xPath = xPath.replace("<", "&lt;");
			xPath = xPath.replace(">", "&gt;");
			return xPath;
		} catch (JAXBException e) {
			System.out.println("Constraint pattern property file loading error");
			e.printStackTrace();
		}
		return null;
	}
	
	private String generateCondXpath(Constraint constraint){
		ConditionsElement cond = constraint.getConditions();
		Iterator<RuleElement> iterator = cond.getElements().iterator();
		String formula = "", xPath = "";	
		if(iterator.hasNext()){
			while (iterator.hasNext()) {
				formula += generateRuleXpath(constraint, (RuleElement) iterator.next());
				if(iterator.hasNext()) formula += " and ";
			}
			
			xPath = "not("+formula+") or "+ formula +" and "; 
		}
		return xPath;
	}

	private String generateErrorMessage(RuleElement element){
		ConstraintPatterns patterns;
		try {
			patterns = MyUtils.getConstraintPatterns();
			String elementName = element.getName();
			String message = null;
			if(element instanceof SimpleRuleElement){
				String value = String.valueOf(((SimpleRuleElement)element).getValue());
				message = patterns.getMessage(elementName).getStandard();
				message = message.replace("$value", value);
			}
			else if(element instanceof StructuredRuleElement){
				IElementOfStructuredElement subElement = ((StructuredRuleElement)element).getElement();
				int minOccurs = 1, maxOccurs = 1000;
				String value = null;
				if(subElement instanceof Value){
					value = ((Value)subElement).toString();
					message = patterns.getMessage(elementName).getStandard();
					message = message.replace("$value", value);
				}
				else if(subElement instanceof INodeElement){
					Iterator<ILeaf> iterator = ((INodeElement)subElement).getElements().iterator();
					String values = "";
					String logic = " or ";
					if(subElement instanceof Annotations)logic = " and ";
					while (iterator.hasNext()) {
						ILeaf leaf = iterator.next();
						values += leaf.toString();
						if(iterator.hasNext()) values += logic;
					}
					if(subElement instanceof Not){
						message = patterns.getMessage(elementName).getNegative();
					}
					else{
						message = patterns.getMessage(elementName).getStandard();
					}
					message = message.replace("$value", values);
				}
			}
			return message;
		} catch (JAXBException e) {
			System.out.println("Constraint pattern property file loading error");
			e.printStackTrace();
		}
		return null;
	}
	
	private File saveToFile(String unformattedSchema, String name){
        FileWriter fStream;
        try {
            new File(MyPreferences.SCHEMA_DIR).mkdir();
            File file = new File(MyPreferences.SCHEMA_DIR + "\\" + name + "_schema.xml");
            fStream = new FileWriter(file);
            BufferedWriter out = new BufferedWriter(fStream);
            out.write(formatXML(unformattedSchema));
            out.close();
            return file;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
	}
	
    private String formatXML(String unformattedXML) {
        try {
            Transformer serializer = SAXTransformerFactory.newInstance().newTransformer();
            serializer.setOutputProperty(OutputKeys.INDENT, "yes");
            serializer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "4");
            Source xmlSource = new SAXSource(new InputSource(new ByteArrayInputStream(unformattedXML.getBytes())));
            StreamResult res = new StreamResult(new ByteArrayOutputStream());
            serializer.transform(xmlSource, res);
            return new String(((ByteArrayOutputStream) res.getOutputStream()).toByteArray());
        } catch (Exception e) {
            return unformattedXML;
        }
    }
}
